/*
 ------------------------------------------------------------------------------
 Copyright (C) 2009-2011 Eternal Games.

 This file is part of the Eternal Tech source code.

 The Eternal Tech source code is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or (at your
 option) any later version.

 The Eternal Tech source code is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 more details.

 You should have received a copy of the GNU General Public License along with
 the Eternal Tech source code; if not, write to the Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 ------------------------------------------------------------------------------
*/


//
// fileSystem.cpp - ...
//

// TODO:
// - Replace Copy and Remove file in the server
// - Use fs_game


#include "../qcommon/qcommon.h"
#include "../include/ZLib/unzip.h"


#define FILES_HASH_SIZE					1024

#define MAX_FILE_HANDLES				32

#define MAX_LIST_FILES					65536

struct file_t {
	char					name[MAX_PATH_LENGTH];
	fsMode_t				mode;

	FILE					*realFile;				// Only one of realFile or
	unzFile					zipFile;				// zipFile will be used
};

struct fileInPack_t {
	char					name[MAX_PATH_LENGTH];
	bool					isDirectory;

	int						size;
	int						offset;

	struct fileInPack_t		*nextHash;
};

struct pack_t {
	char					path[MAX_PATH_LENGTH];
	char					game[MAX_PATH_LENGTH];

	FILE					*handle;

	int						numFiles;
	fileInPack_t			*files;
	fileInPack_t			*filesHashTable[FILES_HASH_SIZE];
};

struct directory_t {
	char					base[MAX_PATH_LENGTH];
	char					game[MAX_PATH_LENGTH];
};

struct searchPath_t {
	pack_t					*pack;					// Only one of pack or
	directory_t				*directory;				// directory will be used

	struct searchPath_t		*next;
};

static file_t				fs_fileHandles[MAX_FILE_HANDLES];

static searchPath_t			*fs_searchPaths;

static int					fs_packFilesCount;

static memoryPool_t			*com_fileSystemMemoryPool;

static cvar_t				*fs_debug;
static cvar_t				*fs_basePath;
static cvar_t				*fs_game;


/*
 ==============================================================================

    UTILITIES

 ==============================================================================
*/


/*
 ==================
 FS_FileLength

 Returns the length of the given FILE
 ==================
*/
static int FS_FileLength (FILE *f)
{
	int		cur, end;

	cur = ftell(f);
	fseek(f, 0, SEEK_END);
	end = ftell(f);
	fseek(f, cur, SEEK_SET);

	return end;
}

/*
 ==================
 FS_BuildOSPath

 Builds a full OS path from the given components
 ==================
*/
static void FS_BuildOSPath (const char *base, const char *game, const char *name, char osPath[MAX_PATH_LENGTH])
{
	Str_SPrintf(osPath, MAX_PATH_LENGTH, "%s/%s/%s", base, game, name);

	// Fix up path separators
	while (*osPath){
		if (*osPath == '/' || *osPath == '\\')
			*osPath = PATH_SEPARATOR_CHAR;

		osPath++;
	}
}

/*
 ==================
 FS_CreateOSPath

 Creates the given OS path if it doesn't exist
 ==================
*/
static void FS_CreateOSPath (char *osPath)
{
	char	*p;

	p = osPath + 1;

	while (*p){
		if (*p == PATH_SEPARATOR_CHAR)
		{
			*p = 0;

			Sys_CreateDirectory(osPath);

			*p = PATH_SEPARATOR_CHAR;
		}

		p++;
	}
}

/*
 ==================
 FS_IsFileNameValid

 Returns true if the given file name is valid
 ==================
*/
static bool FS_IsFileNameValid (const char *name)
{
	int		i;

	if (!name || !name[0])
		Com_Error(ERR_FATAL, "FS_IsFileNameValid: NULL file name");

	if (Str_Length(name) >= MAX_PATH_LENGTH)
		Com_Error(ERR_FATAL, "FS_IsFileNameValid: file name exceeds MAX_PATH_LENGTH");

	// Make absolutely sure that it can't back up the path
	if (Str_FindText(name, "..", true) || Str_FindText(name, "::", true))
	{
		Com_DevPrintf(S_COLOR_RED "FS_IsFileNameValid: illegal file name '%s'\n", name);
		return false;
	}

	// Only allow certain characters in file names
	for (i = 0; name[i]; i++)
	{
		if (name[i] >= 'a' && name[i] <= 'z')
			continue;
		if (name[i] >= 'A' && name[i] <= 'Z')
			continue;
		if (name[i] >= '0' && name[i] <= '9')
			continue;
		if (name[i] == '_' || name[i] == '/' || name[i] == '\\' || name[i] == '.')
			continue;

		Com_DevPrintf(S_COLOR_RED "FS_IsFileNameValid: illegal file name '%s'\n", name);
		return false;
	}

	return true;
}

/*
 ==================
 FS_FileNameCompare

 Ignores case and separator char distinctions
 ==================
*/
static int FS_FileNameCompare (const char *name1, const char *name2)
{
	int		c1, c2;

	do {
		c1 = *name1++;
		c2 = *name2++;

		if (c1 != c2)
		{
			if (c1 >= 'a' && c1 <= 'z')
				c1 -= ('a' - 'A');
			if (c2 >= 'a' && c2 <= 'z')
				c2 -= ('a' - 'A');

			if (c1 == ':' || c1 == '\\')
				c1 = '/';
			if (c2 == ':' || c2 == '\\')
				c2 = '/';

			if (c1 != c2)
			{
				// Names not equal
				if (c1 < c2)
					return -1;
				else
					return 1;
			}
		}
	} while (c1);

	return 0;	// Names are equal
}

/*
 ==================
 FS_CopyFile
 ==================
*/
bool FS_CopyFile2 (const char *srcName, const char *dstName)
{
	char	srcPath[MAX_PATH_LENGTH], dstPath[MAX_PATH_LENGTH];
	FILE	*f1, *f2;
	byte	buffer[65536];
	int		remaining, count;

	// Make sure the file names are valid
	if (!FS_IsFileNameValid(srcName) || !FS_IsFileNameValid(dstName))
		return false;

	FS_BuildOSPath(fs_basePath->value, fs_game->value, srcName, srcPath);
	FS_BuildOSPath(fs_basePath->value, fs_game->value, dstName, dstPath);

	if (fs_debug->integerValue)
		Com_Printf("FS_CopyFile: '%s' to '%s' ('%s%s%s')\n", srcName, dstName, fs_basePath->value, PATH_SEPARATOR_STRING, fs_game->value);

	// Open the files
	f1 = fopen(srcPath, "rb");
	if (!f1)
	{
		Com_DevPrintf(S_COLOR_RED "FS_CopyFile: couldn't open '%s'\n", srcName);
		return false;
	}

	f2 = fopen(dstPath, "wb");
	if (!f2)
	{
		fclose(f1);

		Com_DevPrintf(S_COLOR_RED "FS_CopyFile: couldn't open '%s'\n", dstName);
		return false;
	}

	// Copy in small chunks
	remaining = FS_FileLength(f1);

	while (remaining){
		count = remaining;
		if (count > sizeof(buffer))
			count = sizeof(buffer);

		if (fread(buffer, 1, count, f1) != count)
		{
			Com_DevPrintf(S_COLOR_RED "FS_CopyFile: short read from '%s'\n", srcName);
			break;
		}

		if (fwrite(buffer, 1, count, f2) != count)
		{
			Com_DevPrintf(S_COLOR_RED "FS_CopyFile: short write to '%s'\n", dstName);
			break;
		}

		remaining -= count;
	}

	fclose(f1);
	fclose(f2);

	return (remaining == 0);
}

/*
 ==================
 FS_RenameFile
 ==================
*/
bool FS_RenameFile2 (const char *oldName, const char *newName)
{
	char	oldPath[MAX_PATH_LENGTH], newPath[MAX_PATH_LENGTH];

	// Make sure the file names are valid
	if (!FS_IsFileNameValid(oldName) || !FS_IsFileNameValid(newName))
		return false;

	FS_BuildOSPath(fs_basePath->value, fs_game->value, oldName, oldPath);
	FS_BuildOSPath(fs_basePath->value, fs_game->value, newName, newPath);

	if (fs_debug->integerValue)
		Com_Printf("FS_RenameFile: '%s' to '%s' ('%s%s%s')\n", oldName, newName, fs_basePath->value, PATH_SEPARATOR_STRING, fs_game->value);

	// Rename it
	if (rename(oldPath, newPath))
	{
		Com_DevPrintf(S_COLOR_RED "FS_RenameFile: couldn't rename '%s' to '%s'\n", oldName, newName);
		return false;
	}

	return true;
}

/*
 ==================
 FS_FileExists
 ==================
*/
bool FS_FileExists2 (const char *name)
{
	char	path[MAX_PATH_LENGTH];
	FILE	*f;

	// Make sure the file name is valid
	if (!FS_IsFileNameValid(name))
		return false;

	FS_BuildOSPath(fs_basePath->value, BASE_DIRECTORY, name, path);

	// Check if it exists
	f = fopen(path, "rb");
	if (!f)
	{
		if (fs_debug->integerValue)
			Com_Printf("FS_FileExists: couldn't find '%s'\n", name);

		return false;
	}

	fclose(f);

	if (fs_debug->integerValue)
		Com_Printf("FS_FileExists: '%s' (found in '%s%s%s')\n", name, fs_basePath->value, PATH_SEPARATOR_STRING, BASE_DIRECTORY);

	return true;
}


/*
 ==============================================================================

    FILE HANDLE MANAGEMENT

 ==============================================================================
*/


/*
 ==================
 FS_HandleForFile

 Finds a free fileHandle_t
 ==================
*/
static file_t *FS_HandleForFile (fileHandle_t *f)
{
	file_t	*file;
	int		i;

	for (i = 0, file = fs_fileHandles; i < MAX_FILE_HANDLES; i++, file++)
	{
		if (!file->realFile && !file->zipFile)
			break;
	}

	if (i == MAX_FILE_HANDLES)
		Com_Error(ERR_FATAL, "FS_HandleForFile: none free");

	*f = i + 1;

	return file;
}

/*
 ==================
 FS_GetFileByHandle

 Returns a file_t for the given fileHandle_t
 ==================
*/
static file_t *FS_GetFileByHandle (fileHandle_t f)
{
	file_t	*file;

	if (f <= 0 || f > MAX_FILE_HANDLES)
		Com_Error(ERR_FATAL, "FS_GetFileByHandle: handle out of range");

	file = &fs_fileHandles[f - 1];

	if (!file->realFile && !file->zipFile)
		Com_Error(ERR_FATAL, "FS_GetFileByHandle: invalid handle");

	return file;
}


/*
 ==============================================================================

    FILE IN/OUT FUNCTIONS

 ==============================================================================
*/


/*
 ==================
 FS_Read

 Properly handles partial reads
 ==================
*/
int FS_Read2 (fileHandle_t f, void *buffer, int size)
{
	file_t	*file;
	byte	*ptr;
	int		remaining, count;
	bool	tried = false;

	if (!buffer)
		Com_Error(ERR_FATAL, "FS_Read: NULL buffer");
	if (size < 0)
		Com_Error(ERR_FATAL, "FS_Read: size < 0");

	file = FS_GetFileByHandle(f);

	if (file->mode != FS_READ)
		Com_Error(ERR_FATAL, "FS_Read: cannot read from '%s'", file->name);

	ptr = (byte *)buffer;
	remaining = size;

	while (remaining){
		if (file->realFile)
			count = fread(ptr, 1, remaining, file->realFile);
		else
			count = unzReadCurrentFile(file->zipFile, ptr, remaining);

		if (count == 0)
		{
			if (!tried)
			{
				tried = true;
				continue;
			}

			Com_DevPrintf(S_COLOR_RED "FS_Read: 0 bytes read from '%s'\n", file->name);

			if (fs_debug->integerValue)
				Com_Printf("FS_Read: %i bytes read from '%s'\n", size - remaining, file->name);

			return size - remaining;
		}

		if (count < 0)
			Com_Error(ERR_FATAL, "FS_Read: -1 bytes read from '%s'", file->name);

		ptr += count;
		remaining -= count;
	}

	if (fs_debug->integerValue)
		Com_Printf("FS_Read: %i bytes read from '%s'\n", size, file->name);

	return size;
}

/*
 ==================
 FS_Write

 Properly handles partial writes
 ==================
*/
int FS_Write2 (fileHandle_t f, const void *buffer, int size)
{
	file_t	*file;
	byte	*ptr;
	int		remaining, count;
	bool	tried = false;

	if (!buffer)
		Com_Error(ERR_FATAL, "FS_Write: NULL buffer");
	if (size < 0)
		Com_Error(ERR_FATAL, "FS_Write: size < 0");

	file = FS_GetFileByHandle(f);

	if (file->mode != FS_WRITE && file->mode != FS_APPEND)
		Com_Error(ERR_FATAL, "FS_Write: cannot write to '%s'", file->name);

	ptr = (byte *)buffer;
	remaining = size;

	while (remaining){
		if (file->realFile)
			count = fwrite(ptr, 1, remaining, file->realFile);
		else
			Com_Error(ERR_FATAL, "FS_Write: cannot write to zipped file '%s'", file->name);

		if (count == 0)
		{
			if (!tried)
			{
				tried = true;
				continue;
			}

			Com_DevPrintf(S_COLOR_RED "FS_Write: 0 bytes written to '%s'\n", file->name);

			if (fs_debug->integerValue)
				Com_Printf("FS_Write: %i bytes written to '%s'\n", size - remaining, file->name);

			return size - remaining;
		}

		if (count < 0)
			Com_Error(ERR_FATAL, "FS_Write: -1 bytes written to '%s'", file->name);

		ptr += count;
		remaining -= count;
	}

	if (fs_debug->integerValue)
		Com_Printf("FS_Write: %i bytes written to '%s'\n", size, file->name);

	return size;
}

/*
 ==================
 FS_Printf
 ==================
*/
int FS_Printf (fileHandle_t f, const char *fmt, ...)
{
	file_t	*file;
	va_list	argPtr;
	int		count;

	file = FS_GetFileByHandle(f);

	if (file->mode != FS_WRITE && file->mode != FS_APPEND)
		Com_Error(ERR_FATAL, "FS_Printf: cannot write to '%s'", file->name);

	if (file->realFile)
	{
		va_start(argPtr, fmt);
		count = vfprintf(file->realFile, fmt, argPtr);
		va_end(argPtr);
	}
	else
		Com_Error(ERR_FATAL, "FS_Printf: cannot write to zipped file '%s'", file->name);

	if (count == 0)
	{
		Com_DevPrintf(S_COLOR_RED "FS_Printf: 0 chars written to '%s'\n", file->name);

		if (fs_debug->integerValue)
			Com_Printf("FS_Printf: 0 chars written to '%s'\n", file->name);

		return 0;
	}

	if (count < 0)
		Com_Error(ERR_FATAL, "FS_Printf: -1 chars written to '%s'", file->name);

	if (fs_debug->integerValue)
		Com_Printf("FS_Printf: %i chars written to '%s'\n", count, file->name);

	return count;
}


/*
 ==============================================================================

    FILE OPEN MODES

 ==============================================================================
*/


/*
 ==================
 FS_OpenFileRead

 Returns the size if found, if not, it will return -1
 ==================
*/
static int FS_OpenFileRead (const char *name, FILE **realFile, unzFile *zipFile)
{
	searchPath_t   *searchPath;
	pack_t		   *pack;
	fileInPack_t   *fileInPack;
	directory_t	   *directory;
	char		   path[MAX_PATH_LENGTH];
	int			   size;
	uint		   hashKey;

	// Make sure the file name is valid
	if (!FS_IsFileNameValid(name))
		return -1;

	// Get the hash key for the given file name
	hashKey = Str_HashKey(name, FILES_HASH_SIZE, false);

	// Search through the paths, one element at a time
	for (searchPath = fs_searchPaths; searchPath; searchPath = searchPath->next)
	{
		if (searchPath->pack)
		{
			// Search inside the pack file
			pack = searchPath->pack;

			for (fileInPack = pack->filesHashTable[hashKey]; fileInPack; fileInPack = fileInPack->nextHash)
			{
				if (fileInPack->isDirectory)
					continue;

				if (!FS_FileNameCompare(fileInPack->name, name))
				{
					// Found it!
					if (fs_debug->integerValue)
						Com_Printf("FS_OpenFileRead: '%s' (found in '%s')\n", name, pack->path);

					*realFile = fopen(pack->path, "rb");
					if (*realFile)
					{
						fseek(*realFile, fileInPack->offset, SEEK_SET);

						return fileInPack->size;
					}

					Com_DevPrintf(S_COLOR_RED "FS_OpenFileRead: couldn't reopen '%s'\n", pack->path);

					return -1;
				}
			}
		}
		else if (searchPath->directory)
		{
			// Search in the directory tree
			directory = searchPath->directory;

			// Build full file path
			FS_BuildOSPath(directory->base, directory->game, name, path);

			*realFile = fopen(path, "rb");
			if (*realFile)
			{
				// Found it!
				if (fs_debug->integerValue)
					Com_Printf("FS_OpenFileRead: '%s' (found in '%s%s%s')\n", name, directory->base, PATH_SEPARATOR_STRING, directory->game);

				size = FS_FileLength(*realFile);

				return size;
			}
		}
	}

	// Not found!
	if (fs_debug->integerValue)
		Com_Printf("FS_OpenFileRead: couldn't find '%s'\n", name);

	return -1;
}

/*
 ==================
 FS_OpenFileWrite
 ==================
*/
static int FS_OpenFileWrite (const char *name, FILE **realFile)
{
	char	path[MAX_PATH_LENGTH];

	// Make sure the file name is valid
	if (!FS_IsFileNameValid(name))
		return -1;

	// Create full file path
	FS_BuildOSPath(fs_basePath->value, fs_game->value, name, path);

	// Create any needed subdirectories
	FS_CreateOSPath(path);

	// Open the file
	*realFile = fopen(path, "wb");
	if (*realFile)
	{
		if (fs_debug->integerValue)
			Com_Printf("FS_OpenFileWrite: '%s' (opened in '%s%s%s')\n", name, fs_basePath->value, PATH_SEPARATOR_STRING, fs_game->value);

		return 0;
	}

	if (fs_debug->integerValue)
		Com_Printf("FS_OpenFileWrite: couldn't open '%s'\n", name);

	return -1;
}

/*
 ==================
 FS_OpenFileAppend
 ==================
*/
static int FS_OpenFileAppend (const char *name, FILE **realFile)
{
	char	path[MAX_PATH_LENGTH];

	// Make sure the file name is valid
	if (!FS_IsFileNameValid(name))
		return -1;

	// Create full file path
	FS_BuildOSPath(fs_basePath->value, fs_game->value, name, path);

	// Create any needed subdirectories
	FS_CreateOSPath(path);

	// Open the file
	*realFile = fopen(path, "ab");
	if (*realFile)
	{
		if (fs_debug->integerValue)
			Com_Printf("FS_OpenFileAppend: '%s' (opened in '%s%s%s')\n", name, fs_basePath->value, PATH_SEPARATOR_STRING, fs_game->value);

		return FS_FileLength(*realFile);
	}

	if (fs_debug->integerValue)
		Com_Printf("FS_OpenFileAppend: couldn't open '%s'\n", name);

	return -1;
}

/*
 ==================
 FS_OpenFile
 ==================
*/
int FS_OpenFile (const char *name, fsMode_t mode, fileHandle_t *f)
{
	file_t	*file;
	FILE	*realFile = NULL;
	unzFile	zipFile = NULL;
	int		size;

	// Try to open the file
	switch (mode){
	case FS_READ:
		size = FS_OpenFileRead(name, &realFile, &zipFile);
		break;
	case FS_WRITE:
		size = FS_OpenFileWrite(name, &realFile);
		break;
	case FS_APPEND:
		size = FS_OpenFileAppend(name, &realFile);
		break;
	default:
		Com_Error(ERR_FATAL, "FS_OpenFile: bad mode for '%s'", name);
	}

	if (size == -1)
	{
		*f = 0;

		return -1;
	}

	// Create a new file handle
	file = FS_HandleForFile(f);

	// Fill it in
	Str_Copy(file->name, name, sizeof(file->name));
	file->mode = mode;
	file->realFile = realFile;
	file->zipFile = zipFile;

	return size;
}

/*
 ==================
 FS_CloseFile
 ==================
*/
void FS_CloseFile (fileHandle_t f)
{
	file_t	*file;

	file = FS_GetFileByHandle(f);

	if (file->realFile)
		fclose(file->realFile);
	else
	{
		unzCloseCurrentFile(file->zipFile);
		unzClose(file->zipFile);
	}

	memset(file, 0, sizeof(file_t));
}

/*
 ==================
 FS_ReadFile
 ==================
*/
int FS_ReadFile (const char *name, void **buffer)
{
	fileHandle_t   f;
	byte		   *ptr;
	int			   size;

	size = FS_OpenFile(name, FS_READ, &f);
	if (!f)
	{
		if (buffer)
			*buffer = NULL;

		return -1;
	}

	if (!buffer)
	{
		FS_CloseFile(f);
		return size;
	}

	*buffer = ptr = (byte *)Mem_Alloc(size + 1, com_fileSystemMemoryPool);

	FS_Read2(f, ptr, size);
	FS_CloseFile(f);

	ptr[size] = 0;

	return size;
}

/*
 ==================
 FS_WriteFile
 ==================
*/
bool FS_WriteFile2 (const char *name, const void *buffer, int size)
{
	fileHandle_t   f;

	if (!buffer)
		Com_Error(ERR_FATAL, "FS_WriteFile: NULL buffer");

	FS_OpenFile(name, FS_WRITE, &f);
	if (!f)
		return false;

	FS_Write2(f, buffer, size);
	FS_CloseFile(f);

	return true;
}

/*
 ==================
 FS_FreeFile
 ==================
*/
void FS_FreeFile2 (const void *buffer)
{
	if (!buffer)
		Com_Error(ERR_FATAL, "FS_FreeFile: NULL buffer");

	Mem_Free(buffer);
}


/*
 ==============================================================================

    FILE AND DIRECTORY SCANNING FUNCTIONS

 ==============================================================================
*/


/*
 ==================
 FS_SortFileList
 ==================
*/
static int FS_SortFileList (const void *elem1, const void *elem2)
{
	const char   *name1 = *(const char **)elem1;
	const char	 *name2 = *(const char **)elem2;

	return FS_FileNameCompare(name1, name2);
}

/*
 ==================
 FS_AddFileToList
 ==================
*/
static int FS_AddFileToList (const char *name, const char **files, int fileCount)
{
	int		i;

	if (fileCount == MAX_LIST_FILES)
		return fileCount;   // Too many files

	// Ignore duplicates
	for (i = 0; i < fileCount; i++)
	{
		if (!FS_FileNameCompare(files[i], name))
			return fileCount;   // Already in list
	}

	// Add it
	files[fileCount++] = Mem_DupString(name, com_fileSystemMemoryPool);

	return fileCount;
}

/*
 ==================
 FS_ListFiles
 ==================
*/
const char **FS_ListFiles (const char *path, const char *extension, bool sort, int *numFiles)
{
	searchPath_t   *searchPath;
	pack_t		   *pack;
	fileInPack_t   *fileInPack;
	directory_t	   *directory;
	char		   name[MAX_PATH_LENGTH];
	const char	   **sysFileList;
	int			   sysNumFiles;
	const char	   **fileList;
	const char	   *files[MAX_LIST_FILES];
	int			   fileCount = 0;
	int			   i;

	// Search through the paths, one element at a time
	for (searchPath = fs_searchPaths; searchPath; searchPath = searchPath->next)
	{
		if (searchPath->pack)
		{
			// Search inside the pack file
			pack = searchPath->pack;

			for (i = 0, fileInPack = pack->files; i < pack->numFiles; i++, fileInPack++)
			{
				// Check the path
				Str_ExtractFilePath(fileInPack->name, name, sizeof(name));

				if (FS_FileNameCompare(path, name))
					continue;

				// Check the extension
				Str_ExtractFileExtension(fileInPack->name, name, sizeof(name));

				if (fileInPack->isDirectory)
				{
					if (extension == NULL || FS_FileNameCompare(extension, "/"))
						continue;
				}
				else
				{
					if (extension != NULL && FS_FileNameCompare(extension, name))
						continue;
				}

				// Extract the name
				Str_ExtractFileName(fileInPack->name, name, sizeof(name));

				// Add it
				fileCount = FS_AddFileToList(name, files, fileCount);
			}
		}
		else if (searchPath->directory)
		{
			// Search in the directory tree
			directory = searchPath->directory;

			Str_SPrintf(name, sizeof(name), "%s/%s/%s", directory->base, directory->game, path);

			// Scan the directory
			sysFileList = Sys_ListFiles(name, extension, false, &sysNumFiles);

			// Add the files
			for (i = 0; i < sysNumFiles; i++)
				fileCount = FS_AddFileToList(sysFileList[i], files, fileCount);

			Sys_FreeFileList(sysFileList);
		}
	}

	if (!fileCount)
	{
		*numFiles = 0;
		return NULL;
	}

	// Sort the list if desired
	if (sort)
		qsort(files, fileCount, sizeof(char *), FS_SortFileList);

	// Copy the list
	fileList = (const char **)Mem_Alloc((fileCount + 1) * sizeof(char *), com_fileSystemMemoryPool);

	for (i = 0; i < fileCount; i++)
		fileList[i] = files[i];

	fileList[i] = NULL;

	*numFiles = fileCount;

	return fileList;
}

/*
 ==================
 FS_FreeFileList
 ==================
*/
void FS_FreeFileList (const char **fileList)
{
	int		i;

	if (!fileList)
		return;

	for (i = 0; fileList[i]; i++)
		Mem_Free(fileList[i]);

	Mem_Free(fileList);
}


/*
 ==============================================================================

    PAK FORMAT LOADING

 ==============================================================================
*/


/*
 ==================
 FS_LoadPAK
 ==================
*/
static pack_t *FS_LoadPAK (const char *packPath, const char *packGame)
{
	FILE		   *handle;
	pakHeader_t    header;
	pakFile_t	   info;
	pack_t		   *pack;
	fileInPack_t   *fileInPack;
	int			   i, numFiles;
	uint		   hashKey;

	// Open the file
	handle = fopen(packPath, "rb");
	if (!handle)
		return NULL;

	// Parse the header
	fread(&header, 1, sizeof(pakHeader_t), handle);

	if (LittleLong(header.ident) != PAK_IDENT)
	{
		fclose(handle);
		return NULL;
	}

	header.dirOfs = LittleLong(header.dirOfs);
	header.dirLen = LittleLong(header.dirLen);

	numFiles = header.dirLen / sizeof(pakFile_t);
	if (numFiles <= 0)
	{
		fclose(handle);
		return NULL;
	}

	// Allocate a new pack
	pack = (pack_t *)Mem_Alloc(sizeof(pack_t), com_fileSystemMemoryPool);

	// Fill it in
	Str_Copy(pack->path, packPath, sizeof(pack->path));
	Str_Copy(pack->game, packGame, sizeof(pack->game));
	pack->handle = handle;
	pack->numFiles = numFiles;
	pack->files = fileInPack = (fileInPack_t *)Mem_Alloc(numFiles * sizeof(fileInPack_t), com_fileSystemMemoryPool);

	for (i = 0; i < FILES_HASH_SIZE; i++)
		pack->filesHashTable[i] = NULL;

	// Parse the directory files
	fseek(handle, header.dirOfs, SEEK_SET);

	for (i = 0; i < numFiles; i++)
	{
		fread(&info, 1, sizeof(pakFile_t), handle);

		// Add the file
		Str_Copy(fileInPack->name, info.name, sizeof(fileInPack->name));
		fileInPack->isDirectory = false;
		fileInPack->size = LittleLong(info.fileLen);
		fileInPack->offset = LittleLong(info.filePos);

		// Add to hash table
		hashKey = Str_HashKey(fileInPack->name, FILES_HASH_SIZE, false);

		fileInPack->nextHash = pack->filesHashTable[hashKey];
		pack->filesHashTable[hashKey] = fileInPack;

		// Go to the next file
		fileInPack++;
	}

	return pack;
}


/*
 ==============================================================================

    GAME DIRECTORY SEARCH PATH

 ==============================================================================
*/


/*
 ==================
 FS_AddGameDirectory

 Loads and adds all the pack files found to the search paths, then adds the
 directory to the head of the search paths
 ==================
*/
static void FS_AddGameDirectory (const char *base, const char *game)
{
	searchPath_t   *searchPath;
	pack_t		   *pack;
	directory_t	   *directory;
	char		   path[MAX_PATH_LENGTH];
	const char	   **fileList;
	int			   numFiles;
	int			   i;

	// Check if the directory has already been added
	for (searchPath = fs_searchPaths; searchPath; searchPath = searchPath->next)
	{
		if (searchPath->directory)
		{
			directory = searchPath->directory;

			if (!FS_FileNameCompare(directory->base, base) && !FS_FileNameCompare(directory->game, game))
				return;
		}
	}

	// Scan the directory for pack files
	Str_SPrintf(path, sizeof(path), "%s/%s", base, game);

	fileList = Sys_ListFiles(path, ".pak", true, &numFiles);

	// Add the official pack files
	for (i = 0; i < numFiles; i++)
	{
		// Build full file path
		FS_BuildOSPath(base, game, fileList[i], path);

		// Load it
		pack = FS_LoadPAK(path, game);
		if (!pack)
			continue;

		// Add the pack file to the search paths
		if (fs_packFilesCount == MAX_PACK_FILES)
			Com_Error(ERR_FATAL, "FS_AddGameDirectory: MAX_PACK_FILES hit");

		searchPath = (searchPath_t *)Mem_Alloc(sizeof(searchPath_t), com_fileSystemMemoryPool);

		searchPath->pack = pack;
		searchPath->directory = NULL;
		searchPath->next = fs_searchPaths;
		fs_searchPaths = searchPath;

		fs_packFilesCount++;
	}

	Sys_FreeFileList(fileList);

	// Create a new directory
	directory = (directory_t *)Mem_Alloc(sizeof(directory_t), com_fileSystemMemoryPool);

	Str_Copy(directory->base, base, sizeof(directory->base));
	Str_Copy(directory->game, game, sizeof(directory->game));

	// Add the directory to the search paths
	searchPath = (searchPath_t *)Mem_Alloc(sizeof(searchPath_t), com_fileSystemMemoryPool);

	searchPath->pack = NULL;
	searchPath->directory = directory;
	searchPath->next = fs_searchPaths;
	fs_searchPaths = searchPath;
}


/*
 ==============================================================================

    CONSOLE COMMANDS

 ==============================================================================
*/


/*
 ==================
 FS_ListSearchPaths_f
 ==================
*/
static void FS_ListSearchPaths_f ()
{
	searchPath_t   *searchPath;
	pack_t		   *pack;
	directory_t	   *directory;
	int			   totalPacks = 0, totalFiles = 0;

	Com_Printf("Current search paths:\n");

	for (searchPath = fs_searchPaths; searchPath; searchPath = searchPath->next)
	{
		if (searchPath->pack)
		{
			pack = searchPath->pack;

			Com_Printf("%s (%i files)\n", pack->path, pack->numFiles);

			totalPacks++;
			totalFiles += pack->numFiles;
		}
		else if (searchPath->directory)
		{
			directory = searchPath->directory;

			Com_Printf("%s%s%s\n", directory->base, PATH_SEPARATOR_STRING, directory->game);
		}
	}

	Com_Printf("--------------------\n");
	Com_Printf("%i files in %i PAK files\n", totalFiles, totalPacks);
}


/*
 ==============================================================================

    INITIALIZATION AND SHUTDOWN

 ==============================================================================
*/


// TODO: Add config files execution if the file exists
void FS_Init ()
{
	Com_Printf("-------- File System Initialization --------\n");

	// Create a memory pool
	com_fileSystemMemoryPool = Mem_RegisterPool("File System");

	// Register variables
	fs_debug = CVar_Register("fs_debug", "0", CVAR_BOOL, CVAR_CHEAT, "Print debugging information", 0, 0);
	fs_basePath = CVar_Register("fs_basePath", Sys_DefaultBaseDirectory(), CVAR_STRING, CVAR_INIT, "Base path", 0, 0);
	fs_game = CVar_Register("fs_game", BASE_DIRECTORY, CVAR_STRING, CVAR_SERVERINFO | CVAR_INIT, "Current game directory", 0, 0);

	// Add commands
	Cmd_AddCommand("listSearchPaths", FS_ListSearchPaths_f);

	// Make sure the variables are valid
	if (!fs_basePath->value[0])
		CVar_SetString(fs_basePath, Sys_DefaultBaseDirectory());

	// Add the directories
	FS_AddGameDirectory(fs_basePath->value, BASE_DIRECTORY);

	// Print current search paths
	FS_ListSearchPaths_f();

	// Read the default config file to test if the installation path is valid
	if (FS_ReadFile("default.cfg", NULL) == -1)
		Com_Error(ERR_FATAL, "Couldn't find default.cfg - Check your " ENGINE_NAME " installation");

	// Execute config files
	Cbuf_AddText("exec default.cfg\n");
	Cbuf_Execute();

	Cbuf_AddText("exec Vortex.cfg\n");
	Cbuf_Execute();

	FS_ExecConfig("autoexec.cfg");
	Cbuf_Execute();

	Com_Printf("--------------------------------------------\n");
}

/*
 ==================
 FS_Shutdown
 ==================
*/
void FS_Shutdown ()
{
	file_t		   *file;
	searchPath_t   *searchPath, *nextSearchPath;
	int			   i;

	// Free memory pool
	Mem_FreePool(com_fileSystemMemoryPool);

	// Remove commands
	Cmd_RemoveCommand("listSearchPaths");

	// Close all files
	for (i = 0, file = fs_fileHandles; i < MAX_FILE_HANDLES; i++, file++)
	{
		if (!file->realFile && !file->zipFile)
			continue;

		if (file->realFile)
			fclose(file->realFile);
		else
		{
			unzCloseCurrentFile(file->zipFile);
			unzClose(file->zipFile);
		}

		memset(file, 0, sizeof(file_t));
	}

	// Free search paths
	for (searchPath = fs_searchPaths; searchPath; searchPath = nextSearchPath)
	{
		nextSearchPath = searchPath->next;

		if (searchPath->pack)
		{
			//unzClose(searchPath->pack->handle);

			Mem_Free(searchPath->pack->files);
			Mem_Free(searchPath->pack);
		}
		else if (searchPath->directory)
			Mem_Free(searchPath->directory);

		Mem_Free(searchPath);
	}

	fs_searchPaths = NULL;

	// Reset pack files counter
	fs_packFilesCount = 0;
}